/* Copyright (c) 2011 Glenn Ganz
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */

#import "CSObjectTemplate.h"
#import "CSScriptObject.h"
#import "CSMethodDescription.h"
#import "CSConstructorDescription.h"


@implementation CSObjectTemplate

v8::Handle<v8::Value> convertCSScriptObjectToTemplate(id obj);
v8::Handle<v8::Value> convertObjCObjectToV8(id obj);
id convertV8ToObjCObject(v8::Local<v8::Value> v8Obj, CSType type);
v8::Handle<v8::Value> dispatchStatic(const v8::Arguments& args);
v8::Handle<v8::Value> dispatch(const v8::Arguments& args);
v8::Handle<v8::Value> newObject(const v8::Arguments& args);

v8::Handle<v8::Value> convertCSScriptObjectToTemplate(id obj) {
	v8::HandleScope handle_scope;
    CSObjectTemplate *objTemplate = [[CSObjectTemplate alloc] initWithClass:[obj class]];
	[objTemplate handle]->SetInternalFieldCount(1);
    if ([[obj class] conformsToProtocol:@protocol(CSScriptObject)] == NO) {
        [NSException raise:@"CSObjectTemplateException" format:@"Object does not implement [CSScriptObject]"];
    }
    id <CSScriptObject> obj2 = (id <CSScriptObject>)obj;
	NSArray *methods = [[obj2 class] methodDescriptions];
	NSEnumerator *methodDescriptionEnumerator = [methods objectEnumerator];
	CSMethodDescription *methodDescription;
	while (methodDescription = [methodDescriptionEnumerator nextObject]) {        
		[objTemplate addMethod:methodDescription];
	}
    
	v8::Handle<v8::ObjectTemplate> pers_templ = v8::Persistent<v8::ObjectTemplate>::New([objTemplate handle]);
	v8::Handle<v8::Object> result = pers_templ->NewInstance();
	v8::Handle<v8::External> ptr = v8::External::New(obj);
    
	result->SetInternalField(0, ptr);
    
	return handle_scope.Close(result);
}

v8::Handle<v8::Value> convertObjCObjectToV8(id obj) {
	if ([obj isKindOfClass:[NSString class]]) {
		return v8::String::New([obj UTF8String]);
	}
	else if ([obj isKindOfClass:[NSNumber class]]) {
        if((strcmp([obj objCType], @encode(int))) == 0) {
            return v8::Integer::New([obj intValue]);
        }
        else {
            return v8::Number::New([obj doubleValue]);
        }
    }
	else if ([obj isKindOfClass:[NSArray class]]) {
		v8::Local<v8::Array> array = v8::Array::New([obj count]);
		NSEnumerator *enumerator = [obj objectEnumerator];
		id elem;
		unsigned int i=0;
		while (elem = [enumerator nextObject])
			array->Set(v8::Number::New(i++), convertObjCObjectToV8(elem));
		return array;
	}
    else if ([obj isKindOfClass:[NSNull class]]) {
        return v8::Null();   
    }
	return convertCSScriptObjectToTemplate(obj);
}

id convertV8ToObjCObject(v8::Local<v8::Value> v8, CSType type) {
	switch (type) {
        case CSObject : {
            if (v8->IsString()) {
                v8::String::AsciiValue str(v8);
                return [NSString stringWithUTF8String:*str];
            }
            else if (v8->IsNull()) {
             return [NSNull null];   
            }
            else if (v8->IsTrue() || v8->IsFalse()) {
             return [NSNumber numberWithBool:v8->IsTrue()];   
            }
            else if (v8->IsBoolean()) {
             return [NSNumber numberWithBool:v8->BooleanValue()];   
            }
            else if (v8->IsNumber()) {
             return [NSNumber numberWithDouble:v8->NumberValue()];   
            }
            else if (v8->IsInt32()) {
             return [NSNumber numberWithInt:v8->Int32Value()];   
            }
            else if (v8->IsArray()) {
                NSMutableArray *array = [NSMutableArray array];
                v8::Local<v8::Array> v8array = v8::Local<v8::Array>::Cast( v8->ToObject() );
                
                for (int i = 0; i < v8array->Length(); ++i) {
                    [array addObject:convertV8ToObjCObject(v8array->Get(v8::Number::New(i)), type)];
                }
                return array;
            }
            else if (v8->IsFunction()) {
                //v8::Local<v8::Function> function = v8::Local<v8::Function>::Cast( v8->ToObject() );
                [NSException raise:@"CSObjectTemplateException" format:@"TODO"];                
            }
            else if (v8->IsExternal()) {
                [NSException raise:@"CSObjectTemplateException" format:@"External is not supported"];
            }
            break;
        }
        case CSVoid : {
            return nil;
        }
        default: {
            [NSException raise:@"CSObjectTemplateException" format:@"Cannot convert v8 type to objc type"];
        }
    }
	return nil;
}

v8::Handle<v8::Value> dispatchStatic(const v8::Arguments& args) {
    @try {   
        v8::Local<v8::Object> obj = args.This();
        v8::Local<v8::External> data = v8::Local<v8::External>::Cast(args.Data());
        CSMethodInfo *mInfo = reinterpret_cast<CSMethodInfo *>(data->Value()); 
        NSString    *methodName = mInfo->name;
        CSObjectTemplate *objT= mInfo->temp; 
        CSMethodDescription *md = [objT methodDescriptionWithArgumentCount:args.Length() name:methodName];
        if (md == nil) {
            [NSException raise:@"CSObjectTemplateException" format:@"No static method with name [%@] found with [%d] arguments", methodName, args.Length()];
        }
        
        SEL sel = [md selector];
        Class c = [objT clazz];
        NSArray     *arguments = [md arguments];

        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[c methodSignatureForSelector:sel]];
        [invocation setTarget:c];
        [invocation setSelector:sel];
        for(int i = 0; i < args.Length(); ++i)
        {
            CSType type = [(CSArgument *)[arguments objectAtIndex:i] type];
            id arg = convertV8ToObjCObject(args[i], type);
            [invocation setArgument:&arg atIndex:i+2];
        }
        [invocation invoke];
        switch ([md returnType]) {
            case CSVoid: return v8::Null();
            case CSObject: {
                id ret;
                [invocation getReturnValue:&ret];	
                return convertObjCObjectToV8(ret);
            }
            default:
                [NSException raise:@"CSObjectTemplateException" format:@"Unknown return type"];
        }
    } 
    @catch (NSException *exception) {
        v8::ThrowException(v8::String::New([[exception description] cStringUsingEncoding:NSUTF8StringEncoding]));
    }
}

v8::Handle<v8::Value> dispatch(const v8::Arguments& args) {
    @try {
        v8::Local<v8::Object> obj = args.This();
        v8::Local<v8::External> ptr = v8::Local<v8::External>::Cast(obj->GetInternalField(0));
        id object = (id)ptr->Value();
        
        v8::Local<v8::External> data = v8::Local<v8::External>::Cast(args.Data());
        CSMethodInfo *mInfo = reinterpret_cast<CSMethodInfo *>(data->Value()); 
        NSString    *methodName = mInfo->name;
        CSObjectTemplate *objT= mInfo->temp; 
        CSMethodDescription *md = [objT methodDescriptionWithArgumentCount:args.Length() name:methodName];
        if (md == nil) {
            [NSException raise:@"CSObjectTemplateException" format:@"No method with name [%@] found with [%d] arguments", methodName, args.Length()];
        }

        SEL         sel = [md selector];
        NSArray     *arguments = [md arguments];

        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[object methodSignatureForSelector:sel]];
        [invocation setTarget:object];
        [invocation setSelector:sel];
        for(int i = 0; i < args.Length(); ++i)
        {
            CSType type = [(CSArgument *)[arguments objectAtIndex:i] type];
            id arg = convertV8ToObjCObject(args[i], type);
            [invocation setArgument:&arg atIndex:i+2];
        }
        [invocation invoke];
        switch ([md returnType]) {
            case CSVoid: return v8::Null();
            case CSObject: {
                id ret;
                [invocation getReturnValue:&ret];	
                return convertObjCObjectToV8(ret);
            }
            default:
                [NSException raise:@"CSObjectTemplateException" format:@"Unknown return type"];
        }
    } 
    @catch (NSException *exception) {
        v8::ThrowException(v8::String::New([[exception description] cStringUsingEncoding:NSUTF8StringEncoding]));
    }
}

v8::Handle<v8::Value> newObject(const v8::Arguments& args) {
    @try {
        v8::Local<v8::Object> obj = args.This();
        
        v8::Local<v8::External> data = v8::Local<v8::External>::Cast(args.Data());
        CSMethodInfo *mInfo = reinterpret_cast<CSMethodInfo *>(data->Value()); 
        NSString    *className = mInfo->name;
        CSObjectTemplate *objT= mInfo->temp; 
        Class c = [objT clazz]; 

        //lookup for the right constructor
        CSConstructorDescription *cd = [objT constructorDescriptionWithArgumentCount:args.Length()];
        if (cd == nil) {
            [NSException raise:@"CSObjectTemplateException" format:@"No constructor found with [%d] arguments for class [%@]", args.Length(), className];
        }
        NSArray     *arguments = [cd arguments];

        SEL sel = [cd selector];
        id object = [[c alloc] autorelease];
        
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[object methodSignatureForSelector:sel]];
        [invocation setTarget:object];
        [invocation setSelector:sel];
        for(int i = 0; i < args.Length(); ++i)
        {
            CSType type = [(CSArgument *)[arguments objectAtIndex:i] type];

            id arg = convertV8ToObjCObject(args[i], type);
            [invocation setArgument:&arg atIndex:i+2];
        }
        [invocation invoke];
        id ret;

        [invocation getReturnValue:&ret];	
        return convertObjCObjectToV8(ret);        
    } 
    @catch (NSException *exception) {
        v8::ThrowException(v8::String::New([[exception description] cStringUsingEncoding:NSUTF8StringEncoding]));
    }
}

- initWithClass:(Class)aClass
{
    handle = v8::ObjectTemplate::New();
    clazz = aClass;
    constructorDescriptions = [[NSMutableArray alloc] init];
    methodDescriptions = [[NSMutableArray alloc] init];
    staticMethodDescriptions = [[NSMutableArray alloc] init];
    
    methodInfos = (CSMethodInfo **)malloc(sizeof(CSMethodInfo *) * 512);

    return self;
}

- (void)dealloc
{
    [constructorDescriptions release];
    [methodDescriptions release];
    [staticMethodDescriptions release];
    
    for(int i = 0;i < methodInfosCount;i++) {
        [methodInfos[i]->name release];
        delete methodInfos[i];
    }
    
    free(methodInfos);

    [super dealloc];
}

- (v8::Handle<v8::ObjectTemplate>)handle
{
    return handle;
}

- (void)addConstructor:(CSConstructorDescription *)aConstructorDescription className:(NSString *)className globalTemplate:(CSObjectTemplate *)globalTemplate
{
    Method m = class_getInstanceMethod(clazz, [aConstructorDescription selector]);
    if (m == NULL) {
        [NSException raise:@"CSObjectTemplateException" format:@"Constructor of class [%@] cannot be registered", [aConstructorDescription className]];
    }
        
    if (constructorHandle.IsEmpty()) {
        CSMethodInfo    * methodInfo = new CSMethodInfo;
        methodInfo->name = [className retain];
        methodInfo->temp = self;
        methodInfos[methodInfosCount++] = methodInfo;

        constructorHandle = v8::FunctionTemplate::New(newObject, v8::External::New(methodInfo));
        [globalTemplate handle]->Set(v8::String::New([className cStringUsingEncoding:NSASCIIStringEncoding]), constructorHandle);
    }
        
    [constructorDescriptions addObject:aConstructorDescription];
}

- (CSConstructorDescription *)constructorDescriptionWithArgumentCount:(unsigned int)count
{
    NSEnumerator    *enumerator = [constructorDescriptions objectEnumerator];
    CSConstructorDescription    *current = nil;
    while ((current = [enumerator nextObject]) != nil) {
        if ([[current arguments] count] == count) {
            return current;
        }
    }
    
    return nil;
}

- (void)addMethod:(CSMethodDescription *)aMethodDescription
{
    Method m = class_getInstanceMethod(clazz, [aMethodDescription selector]);
    if (m == NULL) {
        [NSException raise:@"CSObjectTemplateException" format:@"Method [%@] cannot be registered", [aMethodDescription name]];
    }
      
    if ([self methodDescriptionWithNameRegistered:[aMethodDescription name]] == NO) {
        CSMethodInfo    * methodInfo = new CSMethodInfo;
        methodInfo->name = [[aMethodDescription name] retain];
        methodInfo->temp = self;
        methodInfos[methodInfosCount++] = methodInfo;
        handle->Set(v8::String::New([[aMethodDescription name] cStringUsingEncoding:NSASCIIStringEncoding]), v8::FunctionTemplate::New(dispatch, v8::External::New(methodInfo)));
    }
    
    [methodDescriptions addObject:aMethodDescription];
}

- (CSMethodDescription *)methodDescriptionWithArgumentCount:(unsigned int)count name:(NSString *)methodName
{
    NSEnumerator            *enumerator = [methodDescriptions objectEnumerator];
    CSMethodDescription     *current = nil;
    while ((current = [enumerator nextObject]) != nil) {
        if ([[current arguments] count] == count
            && [[current name] isEqualToString:methodName] == YES) {
            return current;
        }
    }
    
    return nil;
}

- (BOOL)methodDescriptionWithNameRegistered:(NSString *)methodName
{
    NSEnumerator            *enumerator = [methodDescriptions objectEnumerator];
    CSMethodDescription     *current = nil;
    
    while ((current = [enumerator nextObject]) != nil) {
        if ([[current name] isEqualToString:methodName] == YES) {
            return YES;
        }
    }
    
    return NO;
}

- (void)addStaticMethod:(CSMethodDescription *)aMethodDescription className:(NSString *)className globalTemplate:(CSObjectTemplate *)globalTemplate
{
    Method m = class_getClassMethod(clazz, [aMethodDescription selector]);
    if (m == NULL) {
        [NSException raise:@"CSObjectTemplateException" format:@"Method [%@] cannot be registered", [aMethodDescription name]];
    }
    
    if (constructorHandle.IsEmpty()) {
        CSMethodInfo    * methodInfo = new CSMethodInfo;
        methodInfo->name = [className retain];
        methodInfo->temp = self;
        methodInfos[methodInfosCount++] = methodInfo;

        constructorHandle = v8::FunctionTemplate::New(newObject, v8::External::New(methodInfo));
        [globalTemplate handle]->Set(v8::String::New([className cStringUsingEncoding:NSASCIIStringEncoding]), constructorHandle);
    }
    
    if ([self staticMethodDescriptionWithNameRegistered:[aMethodDescription name]] == NO) {
        CSMethodInfo    *methodInfo = new CSMethodInfo;
        methodInfo->name = [[aMethodDescription name] retain];
        methodInfo->temp = self;
        methodInfos[methodInfosCount++] = methodInfo;

        constructorHandle->Set(v8::String::New([[aMethodDescription name] cStringUsingEncoding:NSASCIIStringEncoding]), v8::FunctionTemplate::New(dispatchStatic, v8::External::New(methodInfo)));
    }
    
    [staticMethodDescriptions addObject:aMethodDescription];
}

- (CSMethodDescription *)staticMethodDescriptionWithArgumentCount:(unsigned int)count name:(NSString *)methodName
{
    NSEnumerator    *enumerator = [staticMethodDescriptions objectEnumerator];
    CSMethodDescription    *current = nil;
    
    while ((current = [enumerator nextObject]) != nil) {
        if ([[current arguments] count] == count
            && [[current name] isEqualToString:methodName] == YES) {
            return current;
        }
    }
    
    return nil;
}

- (BOOL)staticMethodDescriptionWithNameRegistered:(NSString *)methodName
{
    NSEnumerator    *enumerator = [staticMethodDescriptions objectEnumerator];
    CSMethodDescription    *current = nil;
    while ((current = [enumerator nextObject]) != nil) {
        if ([[current name] isEqualToString:methodName] == YES) {
            return YES;
        }
    }
    
    return NO;
}

- (Class)clazz
{
    return clazz;
}

@end
